package compiler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;

import datastructure.Alternative;
import datastructure.NonTerminal;
import datastructure.Rule;
import datastructure.Term;
import datastructure.Terminal;


/**
 * De parser herkent zinnen van een simpele grammatica. Deze grammatica is
 * G(&lt;expression&gt;)<br>
 * <br>
 * &nbsp;&lt;expression&gt; ::= &lt;term&gt; &lt;expression-tail&gt;<br>
 * &nbsp;&lt;expression-tail&gt; ::= e | &lt;plus-expression&gt; |
 * &lt;minus-term&gt;<br>
 * &nbsp;&lt;plus-expression&gt; ::= PLUS &lt;expression&gt;<br>
 * &nbsp;&lt;minus-term&gt; ::= MINUS &lt;term&gt; &lt;expression-tail&gt;<br>
 * &nbsp;&lt;term&gt; ::= &lt;factor&gt; &lt;term-tail&gt;<br>
 * &nbsp;&lt;term-tail&gt; ::= e | &lt;times-term&gt;<br>
 * &nbsp;&lt;times-term&gt; ::= TIMES &lt;term&gt;<br>
 * &nbsp;&lt;factor&gt; ::= PARENT_LEFT &lt;expression&gt; PARENT_RIGHT<br>
 * &nbsp;&lt;factor&gt; ::= ID | INT | LONG | FLOAT | DOUBLE<br>
 * [Namen in hoofdletters zijn tokens die door de scanner herkend worden.]<br>
 * Het resultaat van parsen is een string representatie van de oorspronkelijke
 * expressie met daaraan haakjes toegevoegd die de volgorde van evaluatie
 * duidelijk maakt. Bijvoorbeeld, de invoer expressie <code>1+2*3</code>
 * resulteert in <code>(1+(2*3))</code>, omdat de vermenigvuldig operator
 * sterker bindt (en dus eerder wordt ge&euml;valueerd) dan de optel operator.
 */
public class Parser implements TokenKinds {

	/**
	 * De scanner die de tokens levert voor deze parser.
	 */
	private Scanner scanner;

	/**
	 * 1 lookahead voor een LL(1) parser.
	 */
	private Token lookahead;
	
	/**
	 * ArrayList die objecten van het type Rule bevatten. Deze worden gecheckt op BNF,
	 * en daarna gereturnd naar de een object van het type Grammatica om te checken op LL(1)
	 */
	private ArrayList<Rule> rules;

	/**
	 * Een fout is opgetreden bij het parsen van de nonterminal.
	 * 
	 * @param nonterminal
	 *            naam van nonterminal
	 * @param message
	 *            foutmelding
	 */
	private void parseError(String nonterminal, String message) {
		System.err.println();
		System.err.println("parse error in <" + nonterminal + "> : " + message);
		System.err.print("lookahead '" + lookahead.getText() + "' ");
		System.err.print("(kind=" + lookahead.getKind() + ") ");
		System.err.print("at " + lookahead.getLine() + "/"
				+ lookahead.getColumn());
		System.err.println();
		throw new RuntimeException();
	}

	/**
	 * Accepteer huidige token van scanner.
	 */
	private void acceptIt() {
		try {
			lookahead = scanner.nextToken();
		} catch (IOException exception) {
			exception.printStackTrace();
			throw new RuntimeException();
		}
	}

	/**
	 * Accepteer huidige token van scanner, mits deze van een bepaald soort is.
	 * 
	 * @param nonterminal
	 *            naam van nonterminal
	 * @param kind
	 *            identifatie van token soort
	 */
	private void accept(String nonterminal, int kind) {
		if (lookahead.getKind() != kind) {
			parseError(nonterminal, "kind=" + kind + " expected");
		} else {
			acceptIt();
		}

	}

	/**
	 * Construeer een nieuwe parser op de input stream.
	 * 
	 * @param in
	 *            input stream van characters
	 */
	public Parser(InputStream in) {
		scanner = new Scanner(new BufferedReader(new InputStreamReader(in)));
		// Initialiseer lookahead met eerste token van scanner
		acceptIt();
	}

	/**
	 * Parse het root symbool van de grammatica.
	 * 
	 * @return het resultaat van parsing (in dit geval een string)
	 */
	public ArrayList<Rule> parseRoot() {
		rules = new ArrayList<Rule>();
		while(lookahead.getKind() != EOF) {
			rules.add(parseRule());
			acceptIt();
		}
		return rules;
	}
	
	private Rule parseRule() {
		Rule rule = new Rule();
		rule.setNonTerminal(parseNonTerminal());
		accept("DefinedAs", ISDEFINEDAS);
		rule.setAlternatives(parseAlternatives());
		accept("EOL", EOL);
		return rule;
	}
	
	private NonTerminal parseNonTerminal() {
		NonTerminal nonTerminal = null;
		if(lookahead.getKind() == NONTERMINAL) {
			nonTerminal = new NonTerminal(lookahead.getText());
			acceptIt();
		} else {
			parseError("NonTerminal", "lookahead unexpected");
		}
		return nonTerminal;
	}
	
	private ArrayList<Alternative> parseAlternatives() {
		ArrayList<Alternative> alternatives = new ArrayList<Alternative>();
		alternatives.add(parseOneAlternative());
		alternatives.addAll(parseOtherAlternatives());
		return alternatives;
	}
	
	private Alternative parseOneAlternative() {
		Alternative alternative = new Alternative();
		if(lookahead.getKind() == EMPTY) {
			alternative.addToTerms(new Terminal(lookahead.getText()));
			acceptIt();
		} else {
			alternative.addToTerms(parseTerm());
			parseRestAlternatives(alternative);
		}
		return alternative;
	}
		
	private Term parseTerm() {
		Term term = null;
		switch(lookahead.getKind()) {
		case NONTERMINAL:
			term = new NonTerminal(lookahead.getText());
			acceptIt();
			break;
		case TERMINAL:
			term = new Terminal(lookahead.getText());
			acceptIt();
			break;
		default:
			parseError("Term", "lookahead unexpected");
		}
		return term;
	}
	
	private void parseRestAlternatives(Alternative alternative) {
		if(checkForTerm()) {
			alternative.addToTerms(parseTerm());
			parseRestAlternatives(alternative);
		}
	}
	
	private ArrayList<Alternative> parseOtherAlternatives() {
		ArrayList<Alternative> alternatives = new ArrayList<Alternative>();
		if(lookahead.getKind() == OR) {
			acceptIt();
			alternatives.addAll(parseAlternatives());
		}
		return alternatives;
	}
	
	private boolean checkForTerm() {
		return lookahead.getKind() == TERMINAL || lookahead.getKind() == NONTERMINAL;
	}
}